Weekend Business Activity Zones in Melbourne
Authored by: SAHITHI PINNAM
Duration: 90 mins
Level: Intermediate
Pre-requisite Skills: Python, Pandas, NumPy, Matplotlib, Folium (basic geospatial)
Scenario
As a business owner or city economic planner in Melbourne, I want to identify where weekend pedestrian activity is highest and where businesses are concentrated, so I can plan opening hours, promotions, staffing, and events to match weekend demand.
As a planner/analyst, I want named examples of nearby hospitality venues (cafés/restaurants) around the busiest zones to ground decisions in real places.
What this use case will teach you
By the end of this use case you will have:
Pulled multiple City of Melbourne datasets with the API v2.1. Cleaned, filtered, and merged business + pedestrian data for weekend analysis. Performed distance-based proximity joins (≤200 m). Built a bar chart of top weekend hotspots and an interactive map with business counts and named cafés. Written business-oriented insights and recommendations.
Introduction
Weekend activity in Melbourne is driven by retail, hospitality, leisure, and events. This project focuses on where people go on weekends and what businesses are nearby. We combine pedestrian sensors (for weekend footfall) with business locations and a named cafés dataset to deliver actionable guidance for Business & Economy stakeholders.
Datasets used
Business Establishments with Address and Industry Classification [https://data.melbourne.vic.gov.au/explore/dataset/business-establishments-with-address-and-industry-classification/information/?disjunctive.industry_anzsic4_description&disjunctive.block_id&disjunctive.clue_small_area&disjunctive.industry_anzsic4_code] Identifier: business-establishments-with-address-and-industry-classification Use: overall business density near hotspots (counts within 200 m).
Cafes and Restaurants with Seating Capacity[https://data.melbourne.vic.gov.au/explore/dataset/cafes-and-restaurants-with-seating-capacity/information/?disjunctive.block_id&disjunctive.industry_anzsic4_code&disjunctive.industry_anzsic4_description&disjunctive.number_of_seats&disjunctive.clue_small_area] Identifier: cafes-and-restaurants-with-seating-capacity Use: named venues (examples) near hotspots to enrich business context.
Pedestrian Counting System – Monthly counts per hour[https://data.melbourne.vic.gov.au/explore/?q=1.%09Pedestrian+Counting+System+–+Monthly+counts+per+hour&sort=modified] Identifier: pedestrian-counting-system-monthly-counts-per-hour Use: hourly counts; filter Saturday/Sunday and aggregate by sensor.
Pedestrian Counting System – Sensor locations[https://data.melbourne.vic.gov.au/explore/dataset/pedestrian-counting-system-sensor-locations/information/] Identifier: pedestrian-counting-system-sensor-locations Use: coordinates for each sensor to map and compute distances.
Importing Datasets
This section imports libraries for data manipulation, visualisation, simple geospatial operations, and API access.
# Libraries
import pandas as pd
import numpy as np
import requests, io, os
import matplotlib.pyplot as plt
import folium
pd.set_option("display.max_colwidth", 200)
Loading the datasets using API v2.1
We define a helper to fetch full CSV exports from the City of Melbourne API v2.1.
def fetch_dataset_csv(base_url, dataset, api_key=None):
suffix = 'exports/csv?delimiter=%3B&list_separator=%2C"e_all=false&with_bom=true'
url = f"{base_url}{dataset}/{suffix}"
params = {'api_key': api_key} if api_key else {}
r = requests.get(url, params=params, timeout=30)
r.raise_for_status()
return pd.read_csv(io.BytesIO(r.content), delimiter=';')
Fetching and Previewing Datasets
We’ll load the four datasets and quickly preview shapes and columns.
API_KEY = os.environ.get('MELBOURNE_API_KEY', '')
BASE_URL = 'https://data.melbourne.vic.gov.au/api/explore/v2.1/catalog/datasets/'
DATASETS = {
"business": "business-establishments-with-address-and-industry-classification",
"cafes": "cafes-and-restaurants-with-seating-capacity",
"pedestrian": "pedestrian-counting-system-monthly-counts-per-hour",
"locations": "pedestrian-counting-system-sensor-locations"
}
business_df = fetch_dataset_csv(BASE_URL, DATASETS["business"], API_KEY)
cafes_df = fetch_dataset_csv(BASE_URL, DATASETS["cafes"], API_KEY)
ped_df = fetch_dataset_csv(BASE_URL, DATASETS["pedestrian"], API_KEY)
loc_df = fetch_dataset_csv(BASE_URL, DATASETS["locations"], API_KEY)
print("Business columns:", business_df.columns.tolist()[:12], "... | rows:", len(business_df))
print("Cafes columns :", cafes_df.columns.tolist()[:12], "... | rows:", len(cafes_df))
print("Ped columns :", ped_df.columns.tolist()[:12], "... | rows:", len(ped_df))
print("Loc columns :", loc_df.columns.tolist()[:12], "... | rows:", len(loc_df))
Business columns: ['census_year', 'block_id', 'property_id', 'base_property_id', 'clue_small_area', 'trading_name', 'business_address', 'industry_anzsic4_code', 'industry_anzsic4_description', 'longitude', 'latitude', 'point'] ... | rows: 393878 Cafes columns : ['census_year', 'block_id', 'property_id', 'base_property_id', 'building_address', 'clue_small_area', 'trading_name', 'business_address', 'industry_anzsic4_code', 'industry_anzsic4_description', 'seating_type', 'number_of_seats'] ... | rows: 63121 Ped columns : ['id', 'location_id', 'sensing_date', 'hourday', 'direction_1', 'direction_2', 'pedestriancount', 'sensor_name', 'location'] ... | rows: 1405267 Loc columns : ['location_id', 'sensor_description', 'sensor_name', 'installation_date', 'note', 'location_type', 'status', 'direction_1', 'direction_2', 'latitude', 'longitude', 'location'] ... | rows: 143
Displaying Dataset Overview
We focus on columns needed for business-first analysis: names (for cafés), coordinates, and pedestrian counts.
business_df = business_df.copy()
cafes_df = cafes_df.copy()
ped_df = ped_df.copy()
loc_df = loc_df.copy()
# Standardise coordinates to numeric
for df in (business_df, cafes_df, loc_df):
if 'latitude' in df.columns:
df['latitude'] = pd.to_numeric(df['latitude'], errors='coerce')
if 'longitude' in df.columns:
df['longitude'] = pd.to_numeric(df['longitude'], errors='coerce')
# Cafes: normalise name column if present
name_cols = [c for c in cafes_df.columns if c.lower() in ['trading_name','business_name','name','organisation']]
if name_cols:
cafes_df.rename(columns={name_cols[0]: 'business_name'}, inplace=True)
else:
cafes_df['business_name'] = "Unnamed Café/Restaurant"
# Drop rows without coordinates
business_df = business_df.dropna(subset=['latitude','longitude']).copy()
cafes_df = cafes_df.dropna(subset=['latitude','longitude']).copy()
loc_df = loc_df.dropna(subset=['latitude','longitude']).copy()
business_df.head(3), cafes_df[['business_name','latitude','longitude']].head(3), loc_df.head(3)
( census_year block_id property_id base_property_id clue_small_area \
0 2010 1101 110843 110843 Docklands
1 2010 1101 110843 110843 Docklands
2 2010 1101 110843 110843 Docklands
trading_name \
0 Vacant
1 Newsxpress
2 Lifestyle Luggage
business_address \
0 163-235 Spencer Street DOCKLANDS 3008
1 Shop 302, Ground , 237-261 Spencer Street DOCKLANDS 3008
2 Shop 102, Level 1, 163-261 Spencer Street DOCKLANDS 3008
industry_anzsic4_code industry_anzsic4_description longitude \
0 0 Vacant Space 144.950564
1 4244 Newspaper and Book Retailing 144.950564
2 4259 Other Personal Accessory Retailing 144.950564
latitude point
0 -37.814509 -37.8145089728263, 144.9505641424
1 -37.814509 -37.8145089728263, 144.9505641424
2 -37.814509 -37.8145089728263, 144.9505641424 ,
business_name latitude longitude
0 Transit Rooftop Bar -37.817778 144.969942
1 Taxi Kitchen -37.817778 144.969942
2 Taxi Riverside -37.817778 144.969942,
location_id sensor_description sensor_name installation_date \
0 3 Melbourne Central Swa295_T 2009-03-25
1 5 Princes Bridge PriNW_T 2009-03-26
2 9 Southern Cross Station Col700_T 2009-03-23
note location_type status direction_1 \
0 NaN Outdoor A North
1 Replace with: 00:6e:02:01:9e:54 Outdoor A North
2 NaN Outdoor A East
direction_2 latitude longitude location
0 South -37.811015 144.964295 -37.81101524, 144.96429485
1 South -37.818742 144.967877 -37.81874249, 144.96787656
2 West -37.819830 144.951026 -37.81982992, 144.95102555 )
Data Cleaning and Processing
We merge pedestrian data with sensor locations, create timestamps, and keep only useful fields for weekend rollups.
# Merge coordinates
ped_df = ped_df.merge(
loc_df[['sensor_name','latitude','longitude','sensor_description']],
on='sensor_name', how='left'
)
# Standardise column names and build datetime
ped_df = ped_df.rename(columns={
'sensing_date': 'date',
'hourday': 'hour',
'pedestriancount': 'hourly_count',
'sensor_description': 'location_name'
})
ped_df['date'] = pd.to_datetime(ped_df['date'], errors='coerce')
ped_df['hour'] = pd.to_numeric(ped_df['hour'], errors='coerce')
ped_df['date_time'] = pd.to_datetime(
ped_df['date'].astype(str) + ' ' + ped_df['hour'].astype('Int64').astype(str) + ':00',
errors='coerce'
)
# Clean coordinate fields
ped_df['latitude'] = pd.to_numeric(ped_df['latitude'], errors='coerce')
ped_df['longitude'] = pd.to_numeric(ped_df['longitude'], errors='coerce')
# Drop invalid rows
ped_df = ped_df.dropna(subset=['latitude','longitude','date_time','hourly_count']).copy()
print("Ped rows after cleaning:", len(ped_df))
ped_df[['location_name','date_time','hourly_count','latitude','longitude']].head(3)
Ped rows after cleaning: 1486330
| location_name | date_time | hourly_count | latitude | longitude | |
|---|---|---|---|---|---|
| 0 | Flinders Ln -Degraves St (North) | 2024-10-27 15:00:00 | 445 | -37.816848 | 144.965598 |
| 1 | 231 Bourke St | 2024-11-13 20:00:00 | 388 | -37.813331 | 144.966756 |
| 2 | 231 Bourke St | 2025-03-12 13:00:00 | 845 | -37.813331 | 144.966756 |
Feature Engineering (Weekend Filter & Aggregation)
We isolate Saturday/Sunday and compute total weekend foot traffic per sensor.
# Create weekday column
ped_df['weekday'] = ped_df['date_time'].dt.day_name()
# Filter for weekends
weekend_df = ped_df[ped_df['weekday'].isin(['Saturday', 'Sunday'])].copy()
# **Aggregate total weekend count per full location name**
weekend_summary = (
weekend_df
.groupby(['location_name', 'latitude', 'longitude'], as_index=False)['hourly_count']
.sum()
.rename(columns={'hourly_count': 'total_weekend_count'})
.sort_values('total_weekend_count', ascending=False)
)
# top 10 busiest weekend locations
top10_weekend = (
weekend_summary
.head(10)
.reset_index(drop=True)
)
top10_weekend.index = top10_weekend.index + 1
top10_weekend.index.name = "Rank"
# Display final ranked table
top10_weekend
| location_name | latitude | longitude | total_weekend_count | |
|---|---|---|---|---|
| Rank | ||||
| 1 | Southbank | -37.820187 | 144.965085 | 7817620 |
| 2 | Flinders La-Swanston St (West) | -37.816686 | 144.966897 | 6770532 |
| 3 | State Library - New | -37.810578 | 144.964443 | 6296885 |
| 4 | Elizabeth St - Flinders St (East) - New footpath | -37.817980 | 144.965034 | 5876190 |
| 5 | Melbourne Central-Elizabeth St (East) | -37.812585 | 144.962578 | 4722217 |
| 6 | Town Hall (West) | -37.814880 | 144.966088 | 4553044 |
| 7 | Little Collins St-Swanston St (East) | -37.814141 | 144.966094 | 4085416 |
| 8 | Melbourne Central | -37.811015 | 144.964295 | 4074855 |
| 9 | Melbourne Convention Exhibition Centre | -37.824018 | 144.956044 | 4038193 |
| 10 | The Arts Centre | -37.821299 | 144.968793 | 3899753 |
Verify Data Cleaning
A quick look at the top 10 weekend hotspots.
top10_weekend[['location_name','total_weekend_count']].head(10)
| location_name | total_weekend_count | |
|---|---|---|
| Rank | ||
| 1 | Southbank | 7817620 |
| 2 | Flinders La-Swanston St (West) | 6770532 |
| 3 | State Library - New | 6296885 |
| 4 | Elizabeth St - Flinders St (East) - New footpath | 5876190 |
| 5 | Melbourne Central-Elizabeth St (East) | 4722217 |
| 6 | Town Hall (West) | 4553044 |
| 7 | Little Collins St-Swanston St (East) | 4085416 |
| 8 | Melbourne Central | 4074855 |
| 9 | Melbourne Convention Exhibition Centre | 4038193 |
| 10 | The Arts Centre | 3899753 |
Data Subsetting
We will compute two business-oriented metrics around each hotspot (within 200 m): 1) Total nearby businesses (from Business Establishments) 2) Named cafés/restaurants
def haversine_np(lat1, lon1, lat2, lon2):
"""Vectorized Haversine distance in meters."""
R = 6371000.0
phi1, phi2 = np.radians(lat1), np.radians(lat2)
dphi = np.radians(lat2 - lat1)
dlambda = np.radians(lon2 - lon1)
a = np.sin(dphi/2.0)**2 + np.cos(phi1)*np.cos(phi2)*np.sin(dlambda/2.0)**2
return 2*R*np.arcsin(np.sqrt(a))
nearby_business_counts = []
example_cafes = []
for _, s in top10_weekend.iterrows():
lat0, lon0 = s['latitude'], s['longitude']
biz_box = business_df[
(business_df['latitude'] >= lat0-0.002) &
(business_df['latitude'] <= lat0+0.002) &
(business_df['longitude'] >= lon0-0.002) &
(business_df['longitude'] <= lon0+0.002)
]
d_biz = haversine_np(lat0, lon0, biz_box['latitude'].values, biz_box['longitude'].values)
count_biz = int((d_biz <= 200).sum())
nearby_business_counts.append(count_biz)
cafe_box = cafes_df[
(cafes_df['latitude'] >= lat0-0.002) &
(cafes_df['latitude'] <= lat0+0.002) &
(cafes_df['longitude'] >= lon0-0.002) &
(cafes_df['longitude'] <= lon0+0.002)
]
d_cafe = haversine_np(lat0, lon0, cafe_box['latitude'].values, cafe_box['longitude'].values)
nearby_cafes = cafe_box[d_cafe <= 200]
names = ', '.join(nearby_cafes['business_name'].dropna().astype(str).head(3).tolist())
example_cafes.append(names)
top10_weekend['nearby_businesses'] = nearby_business_counts
top10_weekend['example_cafes'] = example_cafes
top10_weekend[['location_name','total_weekend_count','nearby_businesses','example_cafes']]
| location_name | total_weekend_count | nearby_businesses | example_cafes | |
|---|---|---|---|---|
| Rank | ||||
| 1 | Southbank | 7817620 | 4292 | Miyako Japanese Cuisine & Teppanyaki, Bluetrain, The Deck |
| 2 | Flinders La-Swanston St (West) | 6770532 | 20034 | RMB Cafe, The Mill House Bar, Il Tempo 2 Pasta |
| 3 | State Library - New | 6296885 | 12805 | Gong Cha, Harajuku Crepes, Theobroma Chocolate Lounge |
| 4 | Elizabeth St - Flinders St (East) - New footpath | 5876190 | 12989 | Subway, Mad Mex Flinders Lane, RMB Cafe |
| 5 | Melbourne Central-Elizabeth St (East) | 4722217 | 17257 | La Belle Miette, Le Petite Creperie, Big Boy BBQ |
| 6 | Town Hall (West) | 4553044 | 23571 | Health Cosmos, Box On Collins Restaurant, La Vita Buona 'The Good Life' Cellar Bar Deli |
| 7 | Little Collins St-Swanston St (East) | 4085416 | 18753 | Oh Deer, Salero Kito Padang Melbourne, Sambal Kampung |
| 8 | Melbourne Central | 4074855 | 15982 | 1000 Wat, 8 Bit, Gong Cha |
| 9 | Melbourne Convention Exhibition Centre | 4038193 | 170 | Crown Plaza, Crown Plaza, Crown Plaza |
| 10 | The Arts Centre | 3899753 | 516 | Mezz Bar, Jarrah Restaurant & Bar, Sake Restaurant & Bar |
Data Exploration and Visualisation
Visualisation of Top Weekend Hotspots (Bar Chart)
import matplotlib.pyplot as plt
import matplotlib.ticker as ticker
# Calculate nearby cafés count for each hotspot
cafe_counts = []
for _, sensor in top10_weekend.iterrows():
subset_cafes = cafes_df[
(cafes_df['latitude'] >= sensor['latitude']-0.002) &
(cafes_df['latitude'] <= sensor['latitude']+0.002) &
(cafes_df['longitude'] >= sensor['longitude']-0.002) &
(cafes_df['longitude'] <= sensor['longitude']+0.002)
]
distances = haversine_np(sensor['latitude'], sensor['longitude'],
subset_cafes['latitude'].values, subset_cafes['longitude'].values)
cafe_counts.append((distances <= 200).sum())
top10_weekend['nearby_cafes'] = cafe_counts
# Create 3 subplots (horizontal bars)
fig, axes = plt.subplots(1, 3, figsize=(22, 10), sharey=True)
# Foot traffic
axes[0].barh(top10_weekend['location_name'], top10_weekend['total_weekend_count'], color='steelblue')
axes[0].set_title("Top 10 Weekend Pedestrian Hotspots", fontsize=14)
axes[0].set_xlabel("Total Weekend Foot Traffic")
axes[0].xaxis.set_major_formatter(ticker.FuncFormatter(lambda x, p: format(int(x), ',')))
axes[0].invert_yaxis()
for i, v in enumerate(top10_weekend['total_weekend_count']):
axes[0].text(v + 50000, i, f"{v:,}", va='center', fontsize=8, color='black')
# Nearby businesses
axes[1].barh(top10_weekend['location_name'], top10_weekend['nearby_businesses'], color='orange')
axes[1].set_title("Nearby Businesses within 200m", fontsize=14)
axes[1].set_xlabel("Number of Nearby Businesses")
axes[1].xaxis.set_major_formatter(ticker.FuncFormatter(lambda x, p: format(int(x), ',')))
axes[1].invert_yaxis()
for i, v in enumerate(top10_weekend['nearby_businesses']):
axes[1].text(v + 50, i, f"{v:,}", va='center', fontsize=8, color='black')
# Nearby cafés
axes[2].barh(top10_weekend['location_name'], top10_weekend['nearby_cafes'], color='green')
axes[2].set_title("Nearby Cafés/Restaurants within 200m", fontsize=14)
axes[2].set_xlabel("Number of Nearby Cafés")
axes[2].xaxis.set_major_formatter(ticker.FuncFormatter(lambda x, p: format(int(x), ',')))
axes[2].invert_yaxis()
for i, v in enumerate(top10_weekend['nearby_cafes']):
axes[2].text(v + 5, i, f"{v:,}", va='center', fontsize=8, color='black')
plt.subplots_adjust(wspace=0.4)
plt.tight_layout()
plt.show()
Weekend vs Weekday Comparison
To highlight the importance of weekend activity, comparing total pedestrian counts at the busiest five locations between weekdays and weekends. This helps show how weekend demand differs from normal weekdays.
# Add day_type column
ped_df['day_of_week'] = ped_df['date_time'].dt.day_name()
ped_df['day_type'] = ped_df['day_of_week'].apply(
lambda x: "Weekend" if x in ["Saturday","Sunday"] else "Weekday"
)
# Group by descriptive location and day_type
compare_avg = ped_df.groupby(["location_name","day_type"])["hourly_count"].mean().unstack()
# Top 5 busiest locations
compare_top5_avg = compare_avg.loc[
compare_avg.sum(axis=1).sort_values(ascending=False).head(5).index
]
compare_top5_avg.plot(
kind="bar",
figsize=(10,6),
color=["#1f77b4", "#ff7f0e"]
)
plt.title("Average Daily Pedestrian Counts: Weekend vs Weekday (Top 5 Locations)", fontsize=14)
plt.ylabel("Average Pedestrian Count per Day")
plt.xlabel("Location")
plt.xticks(rotation=45, ha="right")
plt.legend(title="Day Type")
plt.tight_layout()
plt.show()
# Print percentage differences with names
percent_diff = (
(compare_top5_avg["Weekend"] - compare_top5_avg["Weekday"])
/ compare_top5_avg["Weekday"]
) * 100
print("\n--- Weekend vs Weekday % Difference (Top 5 Locations) ---\n")
for loc, diff in percent_diff.items():
if diff > 0:
print(f"{loc}: +{diff:.1f}% higher on weekends")
else:
print(f"{loc}: {diff:.1f}% lower on weekends")
--- Weekend vs Weekday % Difference (Top 5 Locations) --- Flinders La-Swanston St (West): +6.9% higher on weekends Southbank: +14.2% higher on weekends Elizabeth St - Flinders St (East) - New footpath: -5.8% lower on weekends 368 Elizabeth Street: +2.3% higher on weekends State Library - New: +9.1% higher on weekends
Business Composition in Busiest Weekend Location
# Get busiest location details
busiest_lat = top10_weekend.iloc[0]['latitude']
busiest_lon = top10_weekend.iloc[0]['longitude']
busiest_name = top10_weekend.iloc[0]['location_name']
# Filter all businesses within 200m of this location
distances_all = haversine_np(
busiest_lat, busiest_lon,
business_df['latitude'].values,
business_df['longitude'].values
)
nearby_all_businesses = business_df[distances_all <= 200]
# Load cafés dataset if not already loaded
CAFES_DATASET = 'cafes-and-restaurants-with-seating-capacity'
cafes_df = fetch_dataset_csv(BASE_URL, CAFES_DATASET, API_KEY)
# Convert coordinates to numeric for cafés dataset
cafes_df['latitude'] = pd.to_numeric(cafes_df['latitude'], errors='coerce')
cafes_df['longitude'] = pd.to_numeric(cafes_df['longitude'], errors='coerce')
cafes_df = cafes_df.dropna(subset=['latitude', 'longitude'])
# Filter cafes/restaurants within 200m
distances_cafes = haversine_np(
busiest_lat, busiest_lon,
cafes_df['latitude'].values,
cafes_df['longitude'].values
)
nearby_cafes = cafes_df[distances_cafes <= 200]
# Prepare counts for pie chart
cafe_count = len(nearby_cafes)
other_count = len(nearby_all_businesses) - cafe_count
# Plot pie chart
plt.figure(figsize=(6, 6))
plt.pie(
[cafe_count, other_count],
labels=['Cafes/Restaurants', 'Other Businesses'],
autopct='%1.1f%%',
startangle=140,
colors=['#ff9999','#66b3ff']
)
plt.title(f"Business Composition within 200m of {busiest_name}")
plt.show()
print(f"{busiest_name} has {cafe_count} cafes/restaurants out of {len(nearby_all_businesses)} total businesses nearby.")
Southbank has 1157 cafes/restaurants out of 4336 total businesses nearby.
Top 5 Cafes/Restaurants near the busiest location
if 'trading_name' in cafes_df.columns:
top5_cafes = nearby_cafes[['trading_name', 'seating_type', 'number_of_seats', 'latitude', 'longitude']].head(5)
elif 'business_name' in cafes_df.columns:
top5_cafes = nearby_cafes[['business_name', 'latitude', 'longitude']].head(5)
else:
top5_cafes = nearby_cafes.head(5)
print(f"\nTop 5 Cafes/Restaurants within 200m of {busiest_name}:\n")
for idx, row in top5_cafes.iterrows():
name = row.get('trading_name', row.get('business_name', 'Unknown'))
seats = row.get('number_of_seats', 'N/A')
seat_info = f" – {seats} seats" if seats != 'N/A' else ""
print(f"- {name}{seat_info}")
Top 5 Cafes/Restaurants within 200m of Southbank: - Miyako Japanese Cuisine & Teppanyaki – 40 seats - Bluetrain – 180 seats - The Deck – 20 seats - Pure South Dining – 20 seats - Grill'd – 30 seats
Visualisation of Hotspots and Nearby Business Context (Interactive Map)
Map popups show weekend foot traffic, total businesses within 200 m, and example cafés.
from branca.element import Template, MacroElement
import folium
import os
# Ensure nearby_businesses column exists
if 'nearby_businesses' not in top10_weekend.columns:
nearby_business_counts = []
for _, s in top10_weekend.iterrows():
lat0, lon0 = s['latitude'], s['longitude']
biz_box = business_df[
(business_df['latitude'] >= lat0-0.002) &
(business_df['latitude'] <= lat0+0.002) &
(business_df['longitude'] >= lon0-0.002) &
(business_df['longitude'] <= lon0+0.002)
]
d_biz = haversine_np(lat0, lon0, biz_box['latitude'].values, biz_box['longitude'].values)
count_biz = int((d_biz <= 200).sum())
nearby_business_counts.append(count_biz)
top10_weekend['nearby_businesses'] = nearby_business_counts
# Ensure example_cafes column exists
if 'example_cafes' not in top10_weekend.columns:
example_cafes = []
for _, s in top10_weekend.iterrows():
lat0, lon0 = s['latitude'], s['longitude']
cafe_box = cafes_df[
(cafes_df['latitude'] >= lat0-0.002) &
(cafes_df['latitude'] <= lat0+0.002) &
(cafes_df['longitude'] >= lon0-0.002) &
(cafes_df['longitude'] <= lon0+0.002)
]
d_cafe = haversine_np(lat0, lon0, cafe_box['latitude'].values, cafe_box['longitude'].values)
nearby_cafes = cafe_box[d_cafe <= 200]
names = ', '.join(nearby_cafes['business_name'].dropna().astype(str).head(3).tolist())
example_cafes.append(names)
top10_weekend['example_cafes'] = example_cafes
# Create map
melbourne_map2 = folium.Map(location=[-37.8136, 144.9631], zoom_start=13)
for _, row in top10_weekend.iterrows():
if row['nearby_cafes'] >= 50:
marker_color = 'darkgreen'
elif row['nearby_cafes'] >= 20:
marker_color = 'orange'
else:
marker_color = 'lightblue'
if row['example_cafes'] and row['example_cafes'] != "—":
li_items = "".join(f"<li>{n}</li>" for n in row['example_cafes'].split(", "))
cafes_list_html = f"<b>Nearby cafés (≤200m):</b><ul style='margin:4px 0 0 18px;'>{li_items}</ul>"
else:
cafes_list_html = "<b>Nearby cafés (≤200m):</b> —"
popup_text = (
f"<b>{row['location_name']}</b><br>"
f"Weekend Foot Traffic: {int(row['total_weekend_count']):,}<br>"
f"Nearby Businesses (≤200m): {int(row['nearby_businesses']):,}<br>"
f"Nearby Cafés (≤200m): {int(row['nearby_cafes']):,}<br>"
f"{cafes_list_html}"
)
folium.Marker(
location=[row['latitude'], row['longitude']],
popup=popup_text,
icon=folium.Icon(color=marker_color, icon='briefcase')
).add_to(melbourne_map2)
legend_html = """
{% macro html(this, kwargs) %}
<div style="
position: fixed; bottom: 50px; left: 50px;
width: 220px; z-index:9999; font-size:14px;
background-color: white; border:2px solid grey; border-radius:8px; padding: 10px;">
<b>Café Density Legend</b><br>
<i class="fa fa-map-marker fa-2x" style="color:darkgreen"></i> 50+ cafés<br>
<i class="fa fa-map-marker fa-2x" style="color:orange"></i> 20–49 cafés<br>
<i class="fa fa-map-marker fa-2x" style="color:lightblue"></i> <20 cafés
</div>
{% endmacro %}
"""
legend = MacroElement()
legend._template = Template(legend_html)
melbourne_map2.get_root().add_child(legend)
# Show interactive map locally
melbourne_map2
# Save interactive HTML
map_html_file = "Top10_Weekend_Map.html"
melbourne_map2.save(map_html_file)
print(f" Interactive map saved as {map_html_file}")
png_file = "Top10_Weekend_Map.png"
if os.path.exists(png_file):
from IPython.display import Image, display
display(Image(png_file))
print("🖼️ Static PNG preview displayed (for GitHub).")
else:
print("⚠️ No static PNG found. If needed, open the HTML map in a browser, take a screenshot, save as Top10_Weekend_Map.png, and commit it.")
âś… Interactive map saved as Top10_Weekend_Map.html
🖼️ Static PNG preview displayed (for GitHub).
📍 Weekend Business Activity Map¶
Static preview is shown below (for GitHub).
➡️ Click here to view the full interactive map
Business-First Insights
We summarise the key weekend hotspots with business presence for decision-making.
def fmt_int(x):
try:
return f"{int(x):,}"
except:
return str(x)
print("\n--- Weekend Business Activity Insights ---\n")
# Busiest location
busiest = top10_weekend.iloc[0]
print(f"- Busiest weekend location: {busiest['location_name']} "
f"with {fmt_int(busiest['total_weekend_count'])} people.")
# Top 3 locations
print("\n- Top 3 weekend hotspots (with nearby businesses & café examples):")
for i in range(min(3, len(top10_weekend))):
r = top10_weekend.iloc[i]
cafes = r.get('example_cafes', '—') or '—'
print(f" {i+1}. {r['location_name']} – {fmt_int(r['total_weekend_count'])} people; "
f"{fmt_int(r.get('nearby_businesses', 0))} businesses; Cafés: {cafes}")
# Recommendations
print("\n- Recommendations:")
print(" • Extend weekend opening hours in identified hotspots.")
print(" • Run targeted weekend promotions where both footfall and business presence are high.")
print(" • Consider pop-ups/events within ≤200 m of top hotspots to capture leisure traffic.")
--- Weekend Business Activity Insights --- - Busiest weekend location: Southbank with 7,817,620 people. - Top 3 weekend hotspots (with nearby businesses & café examples): 1. Southbank – 7,817,620 people; 4,292 businesses; Cafés: Miyako Japanese Cuisine & Teppanyaki, Bluetrain, The Deck 2. Flinders La-Swanston St (West) – 6,770,532 people; 20,034 businesses; Cafés: RMB Cafe, The Mill House Bar, Il Tempo 2 Pasta 3. State Library - New – 6,296,885 people; 12,805 businesses; Cafés: Gong Cha, Harajuku Crepes, Theobroma Chocolate Lounge - Recommendations: • Extend weekend opening hours in identified hotspots. • Run targeted weekend promotions where both footfall and business presence are high. • Consider pop-ups/events within ≤200 m of top hotspots to capture leisure traffic.
Conclusion
This business-first analysis connects weekend pedestrian demand with nearby business supply. It highlights where the city is busiest on weekends and what commercial presence exists around those locations, supporting decisions on rostering, promotions, and event activation.
Observations
Top weekend hotspots cluster around well-known CBD corridors where business density is high. Named cafés near hotspots provide tangible examples for campaign or partnership targeting. Some high-footfall areas have fewer nearby businesses, indicating potential gaps/opportunities.
Recommendations
Prioritise weekend staffing and extended hours around the busiest sensors. Activate local marketing (offers, street promotions) within ≤200 m of top hotspots. Partner with nearby cafés for co-promotions during peak weekend hours. Trial pop-ups in high-footfall zones with relatively lower business counts to test demand. Limitations
The general Business Establishments export here does not include names; we therefore added a cafés dataset to provide named examples. The 200 m radius is a practical default; results will vary with smaller/larger radii. Sensor counts reflect the immediate area; micro-location effects (laneways, arcades) may differ. Future Work
Add other named industry datasets (e.g., bars/pubs, retail outlets) for richer examples. Segment weekend time-of-day patterns (morning vs evening peaks). Test 100 m/300 m radii and compare business impact metrics.
Export
Save the final ranked hotspots with business counts and example cafés for reporting.
out = top10_weekend[['location_name','latitude','longitude',
'total_weekend_count','nearby_businesses','example_cafes']].copy()
out.to_csv("Top10_Weekend_BusinessImpact.csv", index=False)
print("Saved:", "Top10_Weekend_BusinessImpact.csv")
Saved: Top10_Weekend_BusinessImpact.csv
import os
print(os.path.abspath("Top10_Weekend_Map.html"))
/Users/pinnamsahithi/Desktop/Top10_Weekend_Map.html